package com.huawei.admins.platform.gateway.core.server.gateway;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.jetbrains.annotations.NotNull;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.context.ApplicationContext;
import org.springframework.core.log.LogFormatUtils;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.HttpStatusCode;
import org.springframework.http.codec.ServerCodecConfigurer;
import org.springframework.http.server.reactive.HttpHandler;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.http.server.reactive.observation.ServerRequestObservationContext;
import org.springframework.util.StringUtils;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebHandler;
import org.springframework.web.server.adapter.DefaultServerWebExchange;
import org.springframework.web.server.handler.WebHandlerDecorator;
import org.springframework.web.server.i18n.AcceptHeaderLocaleContextResolver;
import org.springframework.web.server.i18n.LocaleContextResolver;
import org.springframework.web.server.session.DefaultWebSessionManager;
import org.springframework.web.server.session.WebSessionManager;
import reactor.core.publisher.Mono;

public class GatewayHttpHandler extends WebHandlerDecorator implements HttpHandler, InitializingBean {

    private static final Log logger = LogFactory.getLog(GatewayHttpHandler.class);

    private ApplicationContext applicationContext;

    private WebSessionManager sessionManager = new DefaultWebSessionManager();

    private ServerCodecConfigurer codecConfigurer;

    private LocaleContextResolver localeContextResolver = new AcceptHeaderLocaleContextResolver();

    /**
     * Create a {@code WebHandlerDecorator} for the given delegate.
     *
     * @param delegate the WebHandler delegate
     */
    public GatewayHttpHandler(WebHandler delegate) {
        super(delegate);
    }

    @Override
    public void afterPropertiesSet() {
        if (logger.isDebugEnabled()) {
            logger.debug("form data and headers will be shown which may lead to unsafe logging of potentially sensitive data.");
        }
    }

    @NotNull
    @Override
    public Mono<Void> handle(@NotNull ServerHttpRequest request, @NotNull ServerHttpResponse response) {

        ServerWebExchange exchange = createExchange(request, response);

        if (logger.isTraceEnabled()) {
            LogFormatUtils.traceDebug(logger, traceOn ->
                    exchange.getLogPrefix() + formatRequest(exchange.getRequest()) +
                            (traceOn ? ", headers=" + formatHeaders(exchange.getRequest().getHeaders()) : ""));
        }

        ServerRequestObservationContext observationContext = new ServerRequestObservationContext(
                exchange.getRequest(), exchange.getResponse(), exchange.getAttributes());
        exchange.getAttributes().put(
                ServerRequestObservationContext.CURRENT_OBSERVATION_CONTEXT_ATTRIBUTE, observationContext);

        return getDelegate().handle(exchange)
                .doOnSuccess(aVoid -> logResponse(exchange))
                .onErrorResume(ex -> handleUnresolvedError(exchange, ex))
                .then(exchange.cleanupMultipart())
                .then(Mono.defer(response::setComplete));
    }

    protected ServerWebExchange createExchange(ServerHttpRequest request, ServerHttpResponse response) {
        return new DefaultServerWebExchange(request, response, sessionManager, codecConfigurer, localeContextResolver);
    }

    protected String formatRequest(ServerHttpRequest request) {
        String rawQuery = request.getURI().getRawQuery();
        String query = StringUtils.hasText(rawQuery) ? "?" + rawQuery : "";
        return "HTTP " + request.getMethod() + " \"" + request.getPath() + query + "\"";
    }

    private void logResponse(ServerWebExchange exchange) {
        LogFormatUtils.traceDebug(logger, traceOn -> {
            HttpStatusCode status = exchange.getResponse().getStatusCode();
            return exchange.getLogPrefix() + "Completed " + (status != null ? status : "200 OK") +
                    (traceOn ? ", headers=" + formatHeaders(exchange.getResponse().getHeaders()) : "");
        });
    }

    private Mono<Void> handleUnresolvedError(ServerWebExchange exchange, Throwable ex) {

        ServerHttpRequest request = exchange.getRequest();
        ServerHttpResponse response = exchange.getResponse();
        String logPrefix = exchange.getLogPrefix();

        // Sometimes a remote call error can look like a disconnected client.
        // Try to set the response first before the "isDisconnectedClient" check.

        if (response.setStatusCode(HttpStatus.INTERNAL_SERVER_ERROR)) {
            logger.error(logPrefix + "500 Server Error for " + formatRequest(request), ex);
            return Mono.empty();
        }
        else {
            // After the response is committed, propagate errors to the server...
            logger.error(logPrefix + "Error [" + ex + "] for " + formatRequest(request) +
                    ", but ServerHttpResponse already committed (" + response.getStatusCode() + ")");
            return Mono.error(ex);
        }
    }

    private String formatHeaders(HttpHeaders responseHeaders) {
        return responseHeaders.toString();
    }

    public void setApplicationContext(ApplicationContext applicationContext) {
        this.applicationContext = applicationContext;
    }

    public ApplicationContext getApplicationContext() {
        return this.applicationContext;
    }

    public WebSessionManager getSessionManager() {
        return sessionManager;
    }

    public void setSessionManager(WebSessionManager sessionManager) {
        this.sessionManager = sessionManager;
    }

    public ServerCodecConfigurer getCodecConfigurer() {
        return codecConfigurer;
    }

    public void setCodecConfigurer(ServerCodecConfigurer codecConfigurer) {
        this.codecConfigurer = codecConfigurer;
    }

    public LocaleContextResolver getLocaleContextResolver() {
        return localeContextResolver;
    }

    public void setLocaleContextResolver(LocaleContextResolver localeContextResolver) {
        this.localeContextResolver = localeContextResolver;
    }
}
